package myutil.classloader;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;

/**
 *
 * @author Hlib_Babii
 */
public class DynamicCompilingClassLoader extends ClassLoader {

    private final String pathToJavaFiles;
    private final String pathToClassFiles;
//    private long interval;
//    private Class aClass = null;

    public DynamicCompilingClassLoader(String pathToJavaFiles, String pathToClassFiles) {
        this.pathToJavaFiles = pathToJavaFiles;
        this.pathToClassFiles = pathToClassFiles;
    }

    /**
     *
     * @param javaFileName name of the file to be compiled
     * @return true if the compilation worked, false otherwise
     * @throws IOException
     */
    private boolean compile(String javaFileName) throws IOException {
        System.out.println("Compiling " + javaFileName + "...");
        Process p = Runtime.getRuntime()
                .exec("javac " + javaFileName + " -d " + pathToClassFiles);
        try {
            p.waitFor();
        } catch (InterruptedException ie) {
            System.out.println(ie);
        }
        int ret = p.exitValue();
        return ret == 0;
    }

    /**
     * Checks whether specified java file older than specified class file\
     * and recompiles java file if so
     * @param javaFilename path to java file
     * @param classFilename path to class file
     * @return true if java file was recompiled, false otherwise
     * @throws ClassNotFoundException 
     */
    private boolean checkAndRecompile(String javaFilename, String classFilename) throws ClassNotFoundException {
        File javaFile = new File(javaFilename);
        File classFile = new File(classFilename);
        if (javaFile.exists()
                && (!classFile.exists()
                || javaFile.lastModified() > classFile.lastModified())) {
            if (!classFile.exists()) {
                System.out.println(classFilename + " not found.");
            } else {
                System.out.println(classFilename + " is out of date.");
            }
            try {
                System.out.println("Recompiling...");
                if (!compile(javaFilename) || !classFile.exists()) {
                    System.out.println("Compile failed!");
                    throw new ClassNotFoundException("Compile failed: " + javaFilename);
                }
            } catch (IOException ie) {
                throw new ClassNotFoundException(ie.toString());
            }
            return true;
        } else if (javaFile.exists()) {
            System.out.println(javaFilename + " is up-to-date:");
            System.out.println("        class - " + classFile.lastModified()
                    + " java - " + javaFile.lastModified());
            return false;
        } else {
            System.out.println(javaFilename + " not found");
            throw new ClassNotFoundException();
        }
    }

    @Override
    public synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
        Class aClass = null;

        try {
            aClass = findClass(name);
        } catch (ClassNotFoundException ex) {
        }
        if (aClass == null) {
            aClass = findSystemClass(name);
        }

        if (resolve && aClass != null) {
            resolveClass(aClass);
        }
        return aClass;
    }

    @Override
    protected Class<?> findClass(final String name) throws ClassNotFoundException {
        final String fileStub = name.replace('.', '\\');
        final String javaFilename = this.pathToJavaFiles + fileStub + ".java";
        final String classFilename = this.pathToClassFiles + fileStub + ".class";

        System.out.println(javaFilename);
        System.out.println(classFilename);

        checkAndRecompile(javaFilename, classFilename);
        final Class aClass = fileToClass(name, classFilename);
//        new Thread(new Runnable() {
//            @Override
//            public void run() {
//                while (true) {
//                    try {
//                        try {
//                            Thread.sleep(interval);
//                        } catch (InterruptedException ex) {
//                        }
//                        if (checkAndRecompile(javaFilename, classFilename)) {
//                            aClass = fileToClass(name, classFilename);
//                            System.out.println("Class changed!");
//                        }
//                    } catch (ClassNotFoundException ex) {}
//                }
//            }
//        }).start();
        return aClass;
    }

    /**
     * Reads bytes from file and creates <tt>Class</tt> object
     * @param name name of the class
     * @param classFileName class file to read from
     * @return <tt>Class</tt> object from class file
     * @throws ClassNotFoundException if file the specified class file
     * doesn't exist
     */
    private Class fileToClass(String name, String classFileName) throws ClassNotFoundException {
        Class aClass = null;
        try {
            byte raw[] = getBytes(classFileName);
            aClass = defineClass(name, raw, 0, raw.length);
        } catch (IOException ex) {} // not failure
        if (aClass == null) {
            throw new ClassNotFoundException(name);
        }
        return aClass;
    }
    
    /**
     * reads bytes from the specified file
     * @param filename name of the file to read 
     * @return array of bytes read from the file
     * @throws IOException 
     */
    private byte[] getBytes(String filename) throws IOException {

        File file = new File(filename);
        long len = file.length();

        byte raw[] = new byte[(int) len];
        try (FileInputStream fin = new FileInputStream(file)) {
            int r = fin.read(raw);
            if (r != len) {
                throw new IOException("Can't read all, " + r + " != " + len);
            }
        }
        
        return raw;
    }
}
